/* CEClassEditor+FileHandling.m				 
 *
 * This object controls the data of a beaker (molecules, cameras, groups etc.)
 * It is the main document of BeakerBoy and controls everything from loading to
 * setting up the browser which does most of the other work.
 *
 * For interface-info see the header file. The comments in this file mostly
 * cover only the real implementation details.
 *
 * Written by: 		Thomas Engel
 * Created:    		23.10.1993 (Copyleft)
 * Last modified: 	12.11.1994
 */

#import <CEClassEditor+FileHandling.h>
#import <CEMethod.h>
#import <Text_MiscExtensions.h>
#import <MiscSources.subproj/MiscStringArray_List.h>
#import <MiscSources.subproj/MiscEmacsText.h>

#import <misckit/MiscString.h>
#import <misckit/MiscStringArray.h>
#import <misckit/MiscSortedList.h>

@implementation CEClassEditor ( FileHandling )

- _try:path withAlternatives:anArray forText:aText andGetStyle:(int *)aStyle andSetPath:realPath
{
	id	ourPath;
	id	theFilename;
	id	aSearchPath;
	int	trys;

	trys = 0;
	*aStyle = CE_FILETYPE_NONE;
	ourPath = [path copy];
	theFilename = [ourPath fileName];

	while( *aStyle == CE_FILETYPE_NONE &&
		   trys <= [anArray count] )
	{
		[self _readFile:ourPath forText:aText 
				andGetStyle:(int *)aStyle andSetPath:realPath];

		aSearchPath = [anArray objectAt:trys];
		[ourPath free];
		ourPath = nil;
		if( aSearchPath != nil )
		{
			// If it is a dot file then we should try the original path
			// again...actualle we shoul just skip it...I know..but this 
			// works too.
			
			if( [aSearchPath cmp:"."] == 0 )
					ourPath = [path pathName];			
			else if( [aSearchPath spotOfStr:"./"] == 0 )
			{
					ourPath = [path pathName];
					[ourPath addChar:[MiscString pathSeparator]];
					[ourPath concatenate:aSearchPath];
					
					// NOTE: We have to remove the local Dot reference
					// because somehow it does not work !!
					
					[ourPath replaceEveryOccurrenceOf:"/./" with:"/"];
			}
			else	ourPath = [aSearchPath copy];
			
			[ourPath addChar:[MiscString pathSeparator]];
			[ourPath concatenate:theFilename];
		}		
		trys++;
	}
	
	if( ourPath != nil ) [ourPath free];
	[theFilename free];
	return self;
}

- _readFile:path forText:aText andGetStyle:(int *)aStyle andSetPath:realPath
{
	// We will try to load the requested file...and try to set the right
	// text type info.
	
	NXStream 	* aStream;

	// By default we won't allow anybody to edit a none existing text.
	// It won't get stored anyway because we do not have a path for
	// it.
	
	[aText setEditable:NO];

	[path replaceTildeWithHome];
	if( [path doesExistInFileSystem] == NO )
		return self;
	
	// Looks like we have a file...so lets get it into our text object.
	// We have to ensure that all the setting are in the proper fashion.
	// Setting the tabs for ASCII Files in one of them
		
	if( [self _isFileRTF:path] )
	{
		*aStyle = CE_FILETYPE_RTF;
		[aText setMonoFont:NO];
	}
	else	
	{
		*aStyle = CE_FILETYPE_ASCII;
		[aText setMonoFont:YES];
	}
	
	[aText setEditable:YES];
 	
	aStream = NXMapFile( [path stringValue], NX_READONLY );
	if( *aStyle == CE_FILETYPE_ASCII )
			[aText readText:aStream];
	else 	[aText readRichText:aStream];
	NXCloseMemory( aStream, NX_FREEBUFFER );
	
	// Now lets remember where we really did load it from.
	
	[realPath takeStringValueFrom:path];
		
	return self; 
}

- (BOOL)_isFileRTF:path
{
	// Tells us if the requested file is a RTF text.
	// Lets get the first characters and test for "{\rtf0"
	// If it is there...we should load the same text as RTF again.
	// Well quite stupid code..

	FILE * 	aStream;
	char	magic[10];
	id		aString;
	BOOL	answer;
	
	magic[6] = 0;
	answer = NO;
	
	if( [path doesExistInFileSystem] )
	{
		aStream = fopen( [path stringValue], "r" );
		fgets( magic, 7, aStream );
		fclose( aStream );
		aString = [MiscString newWithString:magic];
	
		if( [aString cmp:"{\\rtf0"] == 0 ) 
			answer = YES;
		
		[aString free];
	}	
	return answer;
}

- (const char *)filename
{
	return [filename stringValue];
}

/*
 * Here come the methods we will implement to satisfy our window.
 * They are useful to change the inspector and handle oher tasks.
 */

- _parseMethodFile
{
	id	newLine;
	id	newMethod;
	int	lines;
	int	i;

	lines = [headerFile lineFromPosition:[headerFile textLength]];

	for( i=1; i<lines; i++ )
	{
		newLine = [headerFile substringFromLine:i];
		if ( [newLine charAt:0] == '-' ||
			 [newLine charAt:0] == '+' )
		{
			newMethod = [[CEMethod alloc] initFromText:newLine];
			[methodList addObject:newMethod];
		}
		[newLine free];
	}
		
	return self;
}

- _writeText:aText withStyle:(int)aStyle to:path
{
	// If the file does no exist...we will create it.
	// But it there is no real text for that file...we won't save anything !
	// RTFD requires a differnt style of saving !
	
	FILE		* dummyStream;
	NXStream 	* aStream;
	
	if( aStyle == CE_FILETYPE_NONE ) return self;
	
	else if( aStyle == CE_FILETYPE_RTFD )
		return self;
	
	else
	{
		// Well the streams won't truncate a file so we have to do it on
		// our own.
		// We should do a backup of existing files here if requested.
		
		dummyStream = fopen( [path stringValue], "w" );
		if( dummyStream ) fclose( dummyStream );
		
		aStream = NXMapFile( [path stringValue], NX_WRITEONLY );
		switch( aStyle )
		{
			case CE_FILETYPE_RTF:
				[aText writeRichText:aStream];
				break;
			default:
				[aText writeText:aStream];
		}	
		NXSaveToFile( aStream, [path stringValue] );
		NXCloseMemory( aStream, NX_FREEBUFFER );
	}
	return self;
}

- (BOOL)_selectSourceForMethod:aMethod
{
	// This private method searches for the method inside the SourceFile
	// and _selects_ the code-portion of this method.
	// This really is only for finding the part. 
	// Returns success and sets the internal range information.
	
	// Now the text[] part is more then just ugly..sorry.
	
	char	text[10000]; 
	NXSelPt	from;
	NXSelPt	to;
	int 	i;
	int		first;
	int		last;
	BOOL	gotIt;

	// Find the definition of the method.

	_sourceOriginStart = 0;
	_sourceOriginEnd = 0;	
	[sourceFile setSel:0 :0];
	gotIt = [sourceFile findText:[aMethod name] 
					ignoreCase:NO backwards:NO wrap:NO];

	if( ! gotIt ) return NO;

	// Now we assume that people stick to the rules and have the
	// '{' and '}' as the first characters of each line.
	
	[sourceFile getSel:&from :&to];
	[sourceFile getSubstring:text start:from.cp length:9990];
	
	// The first '{' is our start. A '0' terminates the string and the
	// search !

	gotIt = NO;
	first = -1;
	for( i=0; i<9990; i++ )
	{
		if( text[i] == 0 ) 
			break;
		else if( text[i] == '{' )
		{
			gotIt = YES;
			first = i;
			break;
		}
	}
	
	// Now we need a new-line followed by a '}'. A '0' terminates the string
	// and the search ! Same as before.
	
	gotIt = NO;
	last = -1;
	for( i=0; i<9990; i++ )
	{
		if( text[i] == 0 ) 
			break;
		else if( text[i] == '\n' &&
				 text[i+1] == '}' )
		{
			gotIt = YES;
			last = i+2;
			break;
		}
	}
	
	// Now select the right area.
	
	if( gotIt )
	{
		_sourceOriginStart = from.cp+first;
		_sourceOriginEnd = from.cp+last;
		[sourceFile setSel:_sourceOriginStart :_sourceOriginEnd];
	}

	return gotIt;	
}

- checkDocumentation:sender
{
	// Now this method tries to get the documentation up to date.
	// Missing parts will be inserted...etc.pp.
	
	id	aString;
	id	aMethod;
	int	i;
	id	aList;
	
	// Be care full. we have the remember all the Docu and Source settings
	// becaus ehte method s we use will change them !!!
	
	
	// If there is no docu we ask the app to provide the default template.
	
	if( docuStyle == CE_FILETYPE_NONE )
	{
		[[NXApp delegate] copyClassDocuTemplate];
		[docuFile setMonoFont:NO];
		[docuFile setEditable:YES];
		[docuFile selectAll:self];
		[docuFile pasteFrom:_tempPb];

		// Be sure that we now have the right text type and a valid 
		// path to save the docu to.

		docuStyle = CE_FILETYPE_RTF;
		_docuPath = [filename copy];
		[_docuPath cat:".rtf"];

		// The main template does need some customization to be useful
		// First the version

		[docuFile setSel:0 :0];
		
		if( [docuFile findText:"MyVersion" 
					  ignoreCase:NO backwards:NO wrap:NO] )
			[docuFile replaceSel:"0.1"];

		// Then the Date

		if( [docuFile findText:"1994" 
					  ignoreCase:NO backwards:NO wrap:NO] )
			[docuFile replaceSel:"1995"];

		// Now the Copyright part

		if( [docuFile findText:"MyCompany" 
					  ignoreCase:NO backwards:NO wrap:NO] )
			[docuFile replaceSel:"ClassEditor"];

		// Its time for the class name
	
		if( [docuFile findText:"MyClass" 
					  ignoreCase:NO backwards:NO wrap:NO] )
		{
			aString = [filename fileName];
			[docuFile replaceSel:[aString stringValue]];
			[aString free];
		}
		
		// Some header infos please
		
		if( [docuFile findText:"MyClass.h" 
					  ignoreCase:NO backwards:NO wrap:NO] )
		{
			aString = [filename fileName];
			[aString cat:".h"];
			[docuFile replaceSel:[aString stringValue]];
			[aString free];
		}
	}
	
	// Now we should have a documentation...but lets see if there is one
	// for every method. If not we should try to find the right place
	// to place a template.
	
	// We add the in a sorted order. 
	// Because the _addDocu method does not take care of ordering...we
	// have to sort things the other way around.
	
	aList = [MiscSortedList new];
	[aList setSortOrder:Misc_DESCENDING];
	[aList setSortEnabled:YES];

	for( i=0; i<[methodList count];i++ )
		[aList addObject:[methodList objectAt:i]];
	
	for( i=0; i<[aList count];i++ )
	{
		aMethod = [aList objectAt:i];
		if( [self _selectDocuForMethod:aMethod] == 0 )
			[self _addDocuForMethod:aMethod];
	}
	[aList free];
	
	// That's it for today..

	return self;
}

- (BOOL)_selectDocuForMethod:aMethod
{
	// This private method searches for the method inside the DocuFile
	// and _selects_ the docu-portion of this method.
	// This really is only for finding the part. 
	// Returns the offset to the description text. An offset of '0' meens that
	// no method descript was found.
	// "See also:" is included..AND the blank lines below.
	
	// BUG: Now the text[] part is more then just ugly..sorry.
	// This limits us to methods with descriptions shorter then approx 100
	// lines. I hope that is enough.
	// I really should take the time and witch to a NXStream....
	
	char	text[10000]; 
	NXSelPt	from;
	NXSelPt	to;
	NXSelPt	tmpFrom;
	NXSelPt	tmpTo;
	int		first;
	int		last;
	int 	i;
	BOOL	gotIt;
	int		blanks;
	int		instancesStart = 0;
	id		aString;

	// We have to find the right start-selection inside the documentation
	// first. This depends on the type of method.
	
	_docuOriginStart = 0;
	_docuOriginEnd = 0;
	[docuFile setSel:0 :0];
	
	gotIt = [docuFile findText:"Instance Methods" 
							  ignoreCase:NO backwards:NO wrap:NO
							  font:[Font newFont:"Helvetica-Bold" size:18]];
	[docuFile getSel:&tmpFrom :&tmpTo];
	if( gotIt ) instancesStart = tmpFrom.cp;
	
	// If the method is a class method we should search for the right start
	// pos. The instanceStart is necessary anyway..because we need to check if 
	// the found selector belongs to the right category.
	// Becasue this problem can only happen when we search for a class method
	// (they come first) we should be save that way.
	 
	if( ![aMethod isInstanceMethod] )
	{
		[docuFile setSel:0 :0];
		gotIt = [docuFile findText:"Class Methods" 
							  ignoreCase:NO backwards:NO wrap:NO
							  font:[Font newFont:"Helvetica-Bold" size:18]];
	}
	if( !gotIt ) return NO;
	
	// Well finding the right section is not enough.. we have to find the
	// right selector too. And sure...the font has to be correct. We
	// Don't want the find every ref to that method.
	// We also add a newline character to the end of the selector
	// because we are searching for the whole selector..and not
	// for a prefix inside another method.

	aString = [MiscString newWithString:[aMethod selectorName]];
	[aString cat:"\n"];

	gotIt = [docuFile findText:[aString stringValue] 
					ignoreCase:NO 
					backwards:NO 
					wrap:NO
					font:[Font newFont:"Helvetica-Bold" size:14]];
	[aString free];
	if( !gotIt ) return NO;

	// Well now we should have the first selector for that method.
	// Now if the docu was created with CM it should work if we grab for
	// those right numbers of linefeeds.
	// Really nasty. Don't look at it.
	
	[docuFile getSel:&from :&to];
	
	// Now if the "class" method is inside the instance docu section
	// then we really don't have a method !
	
	if( ![aMethod isInstanceMethod] &&
		from.cp > instancesStart &&
		instancesStart > 0 ) 
		return NO;
	
	// Otherwise read the string...brrr...we really should have a stream here.
	
	[docuFile getSubstring:text start:from.cp length:9990];
	
	// Now we need the 2 last new-lines. A '0' terminates the string and the
	// search ! This is how we find the end of the docu.
	
	gotIt = NO;
	last = -1;
	for( i=0; i<9990; i++ )
	{
		if( text[i] == 0 ) 
			break;
		else if( text[i] == '\n' &&
				 text[i+1] == '\n' &&
				 text[i+2] == '\n'  )
		{
			gotIt = YES;
			last = i;
			break;
		}
	}
	if( !gotIt ) return NO;
	
	// We always search for 2 blank lines (but they never come right after
	// each other..look at the styles!). If we don't find a newLine char
	// with the small Helvetica-Bold 3pt font then it seams like we have an
	// old docu style...this qives us one line less to search for.
	// A '0' terminates the string and the
	// search !

	blanks = 1;
	
	if( [docuFile findText:"\n" 
					ignoreCase:NO 
					backwards:NO 
					wrap:NO
					font:[Font newFont:"Helvetica-Bold" size:3]] == YES )
	{
		[docuFile getSel:&tmpFrom :&tmpTo];
		
		// now if that small line really was in the scope of our currnet
		// method..then we have to pass 2 blank lines.
		// Btw..last is always valid because otherwise we won't be here.
		
		if( tmpFrom.cp < from.cp+last ) blanks = 0;
	}

	gotIt = NO;
	first = -1;
	for( i=0; i<9990; i++ )
	{
		if( text[i] == 0 ) 
			break;
		else if( text[i] == '\n' &&
				 text[i+1] == '\n' )
		{
			blanks++;
			if( blanks == 2 ) 
			{
				gotIt = YES;
				first = i+2;
				break;
			}
		}
	}
	
	// Now select the right area.
	
	if( gotIt )
	{	
		_docuOriginStart = from.cp+first;
		_docuOriginEnd = from.cp+last;
 		[docuFile setSel:_docuOriginStart :_docuOriginEnd];
	}
	
	return gotIt;
}

- _addDocuForMethod:aMethod
{
	// This method adds a template for the documentation of a certain method.
	// It DOES corrupt the pasteboard !
	// BUG: We also don't care about sorting ! This is bad !
	
	NXSelPt	from;
	NXSelPt	to;
	int 	i;
	int		paramCount;
	BOOL		gotIt;
	id		tokens;
	id		aString;

	// We have to find the right start-selection inside the documentation
	// first. This depends on the type of method.
	
	[docuFile setSel:0 :0];
	
	if( [aMethod isInstanceMethod] )
			gotIt = [docuFile findText:"Instance Methods" 
							  ignoreCase:NO backwards:NO wrap:NO
							  font:[Font newFont:"Helvetica-Bold" size:18]];
	else	gotIt = [docuFile findText:"Class Methods" 
							  ignoreCase:NO backwards:NO wrap:NO
							  font:[Font newFont:"Helvetica-Bold" size:18]];
	
	
	if( !gotIt ) return self;

	// Now this is ugly...we should sort the methods alpahbetically !!
	// Buggy because we can only HOPE that nobody adds something behind the
	// "* Methods" section.
	
	[docuFile getSel:&from :&to];
	[docuFile setSel:to.cp+2 :to.cp+2];

	[[NXApp delegate] copyMethodDocuTemplate];
	[docuFile pasteFrom:_tempPb];
	[docuFile setSel:to.cp+2 :to.cp+2];

	[docuFile findText:"_CE_myMethod" 
			  ignoreCase:NO backwards:NO wrap:NO];
	[docuFile replaceSel:[aMethod selectorName]];

	[docuFile findText:"_CE_myMethod" 
			  ignoreCase:NO backwards:NO wrap:NO];
	[docuFile getSel:&from :&to];
	[docuFile replaceSel:[aMethod name]];
	
	// First lets fix the method prefix of the class methods and
	// remove the dulicate prefix"
	
	[docuFile setSel:from.cp-2 :from.cp-1];
	
	if( [aMethod isInstanceMethod] )
	{
		[docuFile findText:"- " ignoreCase:NO backwards:NO wrap:NO];
		[docuFile delete:self];
	}
	else
	{
		[docuFile replaceSel:"+"];
		[docuFile findText:"+ " ignoreCase:NO backwards:NO wrap:NO];
		[docuFile delete:self];
	}
	
	// Now its time to fix all the fonts.
	// Lets tokenize the methods name. This will create a list of
	// all the parts that have a different font.
	// We need to know how many parms the method takes to get a simpler
	// algorithm for coosing the fonts.
	
	tokens = [aMethod methodTokens];
	paramCount = [aMethod numberOfArguments];
	
	for( i=0; i<[tokens count]; i++ )
	{
		// Now select the new token...if we fail to find it..lets quit here.
		
		aString = [tokens objectAt:i];
		if( [docuFile findText:[aString stringValue]
			  		  ignoreCase:NO backwards:NO wrap:NO] == NO ) break;
		
		
		// Depending on the type of token we will choose a different font.
		// First check for casts and then for paramerters.
		
		if( [aString grep:")"] )
			[docuFile setSelFont:[Font newFont:"Times-Roman" size:14]];
		
		// If it is not a cast the situation is a little bit trickier.
		// Imagine a - (BOOL)delegate or a simple -free method.
		// The token will be a part of the selector and still have no
		// ':' attached because there ist no argument						
		
		else if( [aString grep:":"] == NO && paramCount > 0 )
		{
			[docuFile setSelFont:[Font newFont:"Times-Italic" size:14]];
		}
	}
	
	// The tokens are bound to die now.
	
	[[tokens freeObjects] free];
	
	return self;
}

@end

/*
 * History: 13.01.95 Buh
 *			
 *
 * Bugs: - ...
 */